package edu.nf.dbutil;

import java.sql.*;

/**
 * - SQL执行器，核心入口类
 * @author yang
 * @date 2024-05-08
 */
public class SqlExecutor {
    private final Connection connection;

    /**
     * 构造方法传入一个Connection对象
     * @param connection
     */
    public SqlExecutor(Connection connection) {
        this.connection = connection;
    }

    /**
     * 执行增删改操作
     * @param sql sql语句
     * @param params sql语句所需要的参数
     * @return  受影响的行数
     * @throws SQLException 运行时产生sql异常
     * 1）判断 连接对象 与 sql语句 是否为空
     * 2）声明SQL预编译器 PreparedStatement（PreparedStatement 是 Statement 的子类，当多次执行相同的SQL语句时，
     * 预编译的SQL语句只需要编译一次，然后可以重复执行，这大大提高了执行效率。Statement每次执行都需要重新编译SQL语句，因此效率较低。
     * PreparedStatement允许执行带有动态参数的SQL语句，这些参数可以在执行SQL语句之前预编译，然后通过setXXX()方法（如setInt、setString等）
     * 设置参数值。这使得在处理大量相同结构的数据时非常方便。PreparedStatement可以有效防止SQL注入攻击。由于PreparedStatement使用占位符（如?）
     * 代替直接在SQL语句中插入参数，然后调用setXXX()方法设置参数值，因此攻击者无法通过输入恶意代码来改变SQL逻辑。）
     * 3）通过 connection.prepareStatement(sql) 预编译发送 sql 并返回 预编译处理器 对象
     * 4）通过自定义传参方法 setParameters(PreparedStatement ps, Object... params) 将参数填充至占位符
     * 5）执行 executeUpdate() 方法返回受影响的行数
     */
    public int executeUpdate(String sql, Object... params){
        if(connection == null){
            throw new RuntimeException("Null connection.");
        }
        if(sql == null || sql.isEmpty()){
            throw new RuntimeException("Null SQL statement.");
        }
        //声明PreparedStatement
        PreparedStatement ps = null;
        try {
            //预编译发送sql并返回ps对象
            ps = connection.prepareStatement(sql);
            //设置参数
            setParameters(ps, params);
            //执行sql返回影响的行数
            return ps.executeUpdate();
        }catch (SQLException e){
            //异常重抛
            throw new RuntimeException("execute fail.", e);
        }finally {
            close(ps);
            close();
        }
    }

    /**
     * 批量操作
     * @param sql sql语句
     * @param params 批量操作
     * @return 多条影响的行数
     * （与执行单条增删改操作不同的是，该方法传入的参数是一个二维数组，在循环该数组的行数时，将单行数据通过
     * setParameters(PreparedStatement ps, Object... params) 方法填充至预编译SQL对象的占位符，
     * 并通过 ps.addBatch() 方法进行缓存，最后通过 ps.executeBatch() 方法批量执行并返回受影响的行数）
     */
    public int[] executeBatch(String sql, Object[][] params){
        if(connection == null){
            throw new RuntimeException("Null connection.");
        }
        if(sql == null || sql.isEmpty()){
            throw new RuntimeException("Null SQL statement.");
        }
        PreparedStatement ps = null;
        try {
            ps = connection.prepareStatement(sql);
            //批量设置参数
            for (Object[] param : params){
                setParameters(ps, param);
                //将PS的操作缓存到内存中，最后再批量提交到数据库中
                ps.addBatch();
            }
            //批量执行
            return ps.executeBatch();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            close(ps);
            close();
        }
    }

    /**
     * 通用查询方法
     * @param sql sql语句
     * @param handler 结果集处理器
     * @param params 查询参数
     * @return 查询结果
     * @param <T>
     * 1）首先判断连接对象 connection，数据库查询语句 sql 和 结果集处理器对象 handler 是否为空并抛出对应异常
     * 2）声明SQL语句编译器 PreparedStatement 对象、结果集 ResultSet 对象和任意类型的查询数据对象 result
     * 3）通过 connection.prepareStatement(sql) 方法创建 PreparedStatement 对象（这个对象表示一个预编译的SQL语句，
     * 当使用 prepareStatement 方法时，可以传递一个包含SQL语句的字符串 sql 作为参数，这个SQL语句可以包含占位符（通常是用 ? 表示的）
     * 之后你可以使用 PreparedStatement 的 setXXX 方法（如 setString、setInt 等）来设置这些占位符的值）
     * 4）通过自定义方法 setParameters(ps,params) 将参数传入SQL语句预编译器
     * 5）通过 ps.executeQuery() 方法覆盖 结果集对象（executeQuery() 方法是 Statement 和 PreparedStatement（Statement的子类）
     * 接口的一部分，而不是 ResultSet 的方法。这个方法是用来执行SQL查询并返回查询结果的，结果集以 ResultSet 对象的形式返回）
     * 6）将结果集交给自定义的结果集处理器 handler.handle(rs) 去处理并返回处理结果
     * 7）返回处理结果 result
     * 8）最后关闭资源
     */
    public <T> T executeQuery(String sql, ResultSetHandler<T> handler, Object... params){
        if(connection == null){
            throw new RuntimeException("Null connection.");
        }
        if (sql == null || sql.isEmpty()) {
            throw new RuntimeException("Null SQL statement.");
        }
        if (handler == null){
            throw new RuntimeException("Null ResultSetHandler.");
        }
        PreparedStatement ps = null;
        ResultSet rs = null;
        //返回的查询数据
        T result = null;
        try {
            ps = connection.prepareStatement(sql);
            //设置参数
            setParameters(ps,params);
            rs = ps.executeQuery();
            //将结果集交给结果集处理器去处理并返回处理结果
            result = handler.handle(rs);
            return result;
        }catch (SQLException e){
            throw new RuntimeException("execute falil.", e);
        }finally {
            close(rs);
            close(ps);
            close();
        }
    }

    /**
     * 设置sql的参数
     * @param ps preparedStatement
     * @param params 参数集合
     * @throws SQLException sql异常
     * 通过传入的 SQL语句编译器对象 ps 和 不定长参数 params，通过 ps.setObject(int parameterIndex（从1开始）, Object x)
     * 方法将给定的 Object 参数设置为 SQL语句 中的指定参数（填入 sql 语句中对应的占位符 (?) ）
     */
    private void setParameters(PreparedStatement ps, Object... params) throws SQLException{
        for(int i = 0; i < params.length; i++) {
            ps.setObject(i + 1, params[i]);
        }
    }

    /**
     * 关闭预编译器资源
     * @param st
     */
    private void close(Statement st){
        if(st != null){
            try {
                st.close();
            } catch (SQLException e) {
                throw new RuntimeException("Close statement fail.", e);
            }
        }
    }

    /**
     * 关闭结果集处理器资源
     * @param rs
     */
    private void close(ResultSet rs){
        if(rs != null){
            try {
                rs.close();
            } catch (SQLException e) {
                throw new RuntimeException("Close result fail.", e);
            }
        }
    }

    /**
     * 关闭连接对象资源
     */
    private void close(){
        if(connection != null){
            try {
                connection.close();
            } catch (SQLException e) {
                throw new RuntimeException("Close connection fail.", e);
            }
        }
    }
}
